Hyperf 协程混淆

核心原则:协程内存模型

要理解数据混淆,首先必须明白 Hyperf 在 Swoole 驱动下的内存模型:

一、什么情况下类的属性会协程混淆?

当一个 类的实例 被多个协程同时读写属性时,就会发生数据混淆。这种情况通常发生在:

  1. 单例(Singleton)服务:这是最常见的场景。Hyperf 的依赖注入默认就是单例的。如果你在某个服务类中用一个普通属性来存储临时状态(如用户ID),那么所有协程(即所有并发请求)访问的都是同一个实例的同一个属性。
#[Injectable]
class UnsafeService {
	// 这个属性在所有协程间共享!
	public $currentUserId;
	
	public function setUserId($id) {
		$this->currentUserId = $id; // 协程A写入 123
		// 在协程A睡眠期间,协程B写入 456
		Co::sleep(0.1);
		// 协程A醒来,读取到的 $this->currentUserId 已经是 456,而不是它之前设置的 123
		return $this->currentUserId; 
	}
}
  1. 被长期持有的对象:例如,一个被放在静态变量全局变量或某个常驻内存数组中的对象,其属性也会被所有能访问到它的协程共享。

总结

只要一个对象实例被多个协程共享,并且该对象包含可变的(mutable)状态(属性),在没有保护的情况下,其属性就会发生协程混淆

二、为什么慎用静态(Static)字段?

因为静态字段是 “协程混淆”的重灾区,本质上是一种全局变量
静态字段不属于任何对象实例,它属于类本身,在类第一次被加载时初始化,并存在于整个进程的生命周期中。因此,所有协程访问的都是同一块内存地址

class CounterService {
// 危险的静态属性!
public static $count = 0;

public static function increment() {
	// 这行代码不是原子操作:
	// 1. 从内存读取 self::$count 的值到寄存器
	// 2. 寄存器中的值 +1
	// 3. 将寄存器的值写回 self::$count 的内存
	// 多个协程同时执行此方法,步骤会交错,导致最终结果小于预期。
	self::$count++;
}
}

// 在 1000 个并发协程中调用
for ($i = 0; $i < 1000; $i++) {
go(function () {
	CounterService::increment();
});
}
// 最终结果 self::$count 很可能远小于 1000

总结

应绝对避免使用静态字段来存储与请求相关的状态数据。它的唯一安全用途是存储一些只读的、应用启动后就不会改变的配置或缓存。

三、数据混淆的情况有哪些?

1. 身份混淆(最常见且危险)

class UserService {
	private $userInfo; // 错误用法!
	
	public function getInfo() {
		// 假设这里是从数据库取数据,然后赋值给 $this->userInfo
		return $this->userInfo; // 这个值会被下一个请求覆盖
	}
}

2. 计算错误/状态混乱

3. 资源混淆

class DbService {
	private $dbConnection;
	
	public function beginTransaction() {
		$this->dbConnection->beginTransaction();
	}
	// 如果协程A beginTransaction,协程B也调用此方法,
	// 会破坏协程A的事务,或者导致连接状态错误。
}

4. 缓存污染

class CacheService {
	private static $cache = [];
	
	public static function set($key, $value) {
		self::$cache[$key] = $value; // 完全不可控的全局缓存
	}
}

正确的解决方案

1. 使用协程上下文(Coroutine Context)

use Hyperf\Context\Context; // Hyperf v3.x+
// use Hyperf\Utils\Context; // Hyperf v2.x

// 设置值,仅在当前协程有效
Context::set('user_info', $userInfo);

// 获取值
$userInfo = Context::get('user_info');

2. 依赖注入时使用短生命周期

#[Injectable(scope: Scope::PROTOTYPE)] // 每次依赖注入时都创建新实例
class StatefulService {
	public $state;
}

3. 使用同步原语保护共享资源

use Swoole\Coroutine\Channel;
use Swoole\Atomic;

// 使用 Channel 做计数器
$chan = new Channel(1);
$chan->push(0); // 初始化值

go(function () use ($chan) {
	$count = $chan->pop();
	$count++;
	$chan->push($count);
});

// 使用 Atomic (最佳选择)
$atomic = new Atomic(0);
$atomic->add(1); // 原子性操作,安全

4. 彻底避免使用静态字段和全局变量存储状态

最终建议: 在 Hyperf 开发中,养成一个思维习惯——默认认为任何对象属性都不是协程安全的,除非你能明确证明它只被一个协程访问(如原型Scope的实例)或已被妥善保护。始终使用 Context 来传递和存储请求上下文数据